/*
 * Copyright 2011 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package fr.onevu.gwt.uibinder.client.impl;

import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.DomEvent;
import com.google.gwt.event.shared.HasHandlers;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.uibinder.client.UiRenderer;

import java.util.HashMap;

/**
 * Abstract implementation of a safe HTML binder to make implementation of
 * generated rendering simpler.
 */
public abstract class AbstractUiRenderer implements UiRenderer {

	/**
	 * Helps handle method dispatch to classes that use UiRenderer.
	 * 
	 * @param <T>
	 *          class that can receive events from a UiRenderer implementation
	 */
	protected abstract static class UiRendererDispatcher<T> implements HasHandlers {
		private T eventTarget;

		private int methodIndex;

		private Element root;

		/**
		 * Maps strings describing event types and field names to methods contained
		 * in type {@code T} (which are indexed by an integer).
		 */
		private HashMap<String, Integer> table;

		/**
		 * Fire an event to the receiver.
		 * 
		 * @param target
		 *          object that will handle the events
		 * @param event
		 *          event to dispatch
		 * @param parentOrRoot
		 *          root element of a previously rendered DOM structure (or its
		 *          parent)
		 */
		protected void fireEvent(T target, NativeEvent event, Element parentOrRoot) {
			if (target == null) {
				throw new NullPointerException("Null event handler received");
			}
			if (event == null) {
				throw new NullPointerException("Null event object received");
			}
			if (parentOrRoot == null) {
				throw new NullPointerException("Null parent received");
			}

			if (!isParentOrRenderer(parentOrRoot, RENDERED_ATTRIBUTE)) {
				return;
			}
			eventTarget = target;
			root = findRootElementOrNull(parentOrRoot, RENDERED_ATTRIBUTE);
			methodIndex = computeDispatchEvent(table, root, event);
			DomEvent.fireNativeEvent(event, this);
		}

		/**
		 * Object that will receive the event.
		 */
		protected T getEventTarget() {
			return eventTarget;
		}

		/**
		 * Index of the method that will receive the event.
		 */
		protected int getMethodIndex() {
			return methodIndex;
		}

		/**
		 * Root Element of a previously rendered DOM structure.
		 */
		protected Element getRoot() {
			return root;
		}

		/**
		 * Initializes the dispatch table if necessary.
		 */
		protected void initDispatchTable(String[] keys, Integer[] values) {
			table = buildDispatchMap(keys, values);
		}
	}

	/**
	 * Marker attribute for DOM structures previously generated by UiRenderer.
	 */
	public static final String RENDERED_ATTRIBUTE = "gwtuirendered";

	/**
	 * Field name used to identify the root element while dispatching events.
	 */
	public static final String ROOT_FAKE_NAME = "^";

	public static final String UI_ID_SEPARATOR = ":";

	private static final int NO_HANDLER_FOUND = -1;

	/**
	 * Build id strings used to identify DOM elements related to ui:fields.
	 * 
	 * @param fieldName
	 *          name of the field that identifies the element
	 * @param uiId
	 *          common part of the identifier for all elements in the rendered DOM
	 *          structure
	 */
	protected static String buildInnerId(String fieldName, String uiId) {
		return uiId + UI_ID_SEPARATOR + fieldName;
	}

	/**
	 * Retrieves a specific element within a previously rendered element.
	 * 
	 * @param parent
	 *          parent element containing the element of interest
	 * @param fieldName
	 *          name of the field to retrieve
	 * @param attribute
	 *          that identifies the root element as such
	 * @return the element identified by {@code fieldName}
	 * 
	 * @throws IllegalArgumentException
	 *           if the {@code parent} does not point to or contains a previously
	 *           rendered element. In DevMode also when the root element is not
	 *           attached to the DOM
	 * @throws IllegalStateException
	 *           parent does not contain an element matching {@code filedName}
	 * 
	 * @throws RuntimeException
	 *           if the root element is not attached to the DOM and not running in
	 *           DevMode
	 * 
	 * @throws NullPointerException
	 *           if {@code parent} == null
	 */
	protected static Element findInnerField(Element parent, String fieldName, String attribute) {
		Element root = findRootElement(parent, attribute);

		if (parent != root && !isRenderedElementSingleChild(root)) {
			throw new IllegalArgumentException("Parent Element of previously rendered element contains more than one child" + " while getting \"" + fieldName + "\"");
		}

		String uiId = root.getAttribute(attribute);
		String renderedId = buildInnerId(fieldName, uiId);

		Element elementById = Document.get().getElementById(renderedId);
		if (elementById == null) {
			if (!isAttachedToDom(root)) {
				throw new RuntimeException("UiRendered element is not attached to DOM while getting \"" + fieldName + "\"");
			} else if (!GWT.isProdMode()) {
				throw new IllegalStateException("\"" + fieldName + "\" not found within rendered element");
			} else {
				// In prod mode we do not distinguish between being unattached or not
				// finding the element
				throw new IllegalArgumentException("UiRendered element is not attached to DOM, or \"" + fieldName + "\" not found within rendered element");
			}
		}
		return elementById;
	}

	/**
	 * Retrieves the root of a previously rendered element contained within the
	 * {@code parent}. The {@code parent} must either contain the previously
	 * rendered DOM structure as its only child, or point directly to the rendered
	 * element root.
	 * 
	 * @param parent
	 *          element containing, or pointing to, a previously rendered DOM
	 *          structure
	 * @param attribute
	 *          attribute name that identifies the root of the DOM structure
	 * @return the root element of the previously rendered DOM structure
	 * 
	 * @throws NullPointerException
	 *           if {@code parent} == null
	 * @throws IllegalArgumentException
	 *           if {@code parent} does not contain a previously rendered element
	 */
	protected static Element findRootElement(Element parent, String attribute) {
		Element root = findRootElementOrNull(parent, attribute);
		if (root == null) {
			throw new IllegalArgumentException("Parent element does not contain a previously rendered element");
		}
		return root;
	}

	/**
	 * Inserts an attribute into the first tag found in a {@code safeHtml}
	 * template. This method assumes that the {@code safeHtml} template begins
	 * with an open HTML tag. {@code SafeHtml} templates produced by UiBinder
	 * always meet these conditions.
	 * <p>
	 * This method does not attempt to ensure {@code atributeName} and
	 * {@code attributeValue} contain safe values.
	 * 
	 * @returns the {@code safeHtml} template with "{@code attributeName}=
	 *          {@code attributeValue}" inserted as an attribute of the first tag
	 *          found
	 */
	protected static SafeHtml stampUiRendererAttribute(SafeHtml safeHtml, String attributeName, String attributeValue) {
		String html = safeHtml.asString();
		int endOfFirstTag = html.indexOf(">");

		assert endOfFirstTag > 1 : "Safe html template does not start with an HTML open tag";

		if (html.charAt(endOfFirstTag - 1) == '/') {
			endOfFirstTag--;
		}

		html = html.substring(0, endOfFirstTag) + " " + attributeName + "=\"" + attributeValue + "\"" + html.substring(endOfFirstTag);
		return SafeHtmlUtils.fromTrustedString(html);
	}

	/**
	 * Converts an array of keys and values into a map.
	 */
	private static HashMap<String, Integer> buildDispatchMap(String[] keys, Integer[] values) {
		HashMap<String, Integer> result = new HashMap<String, Integer>(keys.length);
		for (int i = 0; i < keys.length; i++) {
			result.put(keys[i], values[i]);
		}
		return result;
	}

	/**
	 * Obtains the index of the method that will receive an event.
	 * 
	 * @param table
	 *          event types and field names indexed by the method that can handle
	 *          an event.
	 * @param root
	 *          of a previously rendered DOM structure
	 * @param event
	 *          event to handle
	 * @return index of the method that will process the event or
	 *         NO_HANDLER_FOUND.
	 */
	private static int computeDispatchEvent(HashMap<String, Integer> table, Element root, NativeEvent event) {
		String uiId = root.getAttribute(RENDERED_ATTRIBUTE);

		EventTarget eventTarget = event.getEventTarget();
		if (!Element.is(eventTarget)) {
			return NO_HANDLER_FOUND;
		}

		Element cursor = Element.as(eventTarget);

		while (cursor != null && cursor != root && cursor.getNodeType() != Element.DOCUMENT_NODE) {
			String fieldName = getFieldName(uiId, cursor);
			if (fieldName == null) {
				cursor = cursor.getParentElement();
				continue;
			}
			String key = event.getType() + UI_ID_SEPARATOR + fieldName;
			if (table.containsKey(key)) {
				return table.get(key);
			}
			cursor = cursor.getParentElement();
		}

		if (cursor == root) {
			String key = event.getType() + UI_ID_SEPARATOR + ROOT_FAKE_NAME;
			if (table.containsKey(key)) {
				return table.get(key);
			}
		}

		return NO_HANDLER_FOUND;
	}

	/**
	 * Retrieves the root of a previously rendered element contained within the
	 * {@code parent}. The {@code parent} must either contain the previously
	 * rendered DOM structure as its only child, or point directly to the rendered
	 * element root.
	 * 
	 * @param parent
	 *          element containing, or pointing to, a previously rendered DOM
	 *          structure
	 * @param attribute
	 *          attribute name that identifies the root of the DOM structure
	 * @return the root element of the previously rendered DOM structure or
	 *         <code>null</code> if {@code parent} does not contain a previously
	 *         rendered element
	 * 
	 * @throws NullPointerException
	 *           if {@code parent} == null
	 */
	private static Element findRootElementOrNull(Element parent, String attribute) {
		if (parent == null) {
			throw new NullPointerException("parent argument is null");
		}

		Element rendered;
		if (parent.hasAttribute(attribute)) {
			// The parent is the root
			return parent;
		} else if ((rendered = parent.getFirstChildElement()) != null && rendered.hasAttribute(attribute)) {
			// The first child is the root
			return rendered;
		} else {
			return null;
		}
	}

	/**
	 * Obtains the field name of a previously rendered DOM Element.
	 * 
	 * @param uiId
	 *          identifier of the fields contained in a previously rendered DOM
	 *          structure
	 * @param element
	 *          which may correspond to {@code ui:field}
	 * @return the field name or {@code null} if the {@code element} does not have
	 *         an id attribute as would be produced by
	 *         {@link #buildInnerId(String, String)}) with {@code fieldName} and
	 *         {@code uiId}
	 */
	private static String getFieldName(String uiId, Element element) {
		String id = element.getId();
		if (id == null) {
			return null;
		}
		int split = id.indexOf(UI_ID_SEPARATOR);
		return split != -1 && uiId.length() == split && id.startsWith(uiId) ? id.substring(split + 1) : null;
	}

	/**
	 * In DevMode, walks up the parents of the {@code rendered} element to
	 * ascertain that it is attached to the document. Always returns
	 * <code>true</code> in ProdMode.
	 */
	private static boolean isAttachedToDom(Element rendered) {
		if (GWT.isProdMode()) {
			return true;
		}

		Element body = Document.get().getBody();

		while (rendered != null && rendered.hasParentElement() && !body.equals(rendered)) {
			rendered = rendered.getParentElement();
		}
		return body.equals(rendered);
	}

	/**
	 * Implements {@link UiRenderer#isParentOrRenderer(Element)} .
	 */
	private static boolean isParentOrRenderer(Element parent, String attribute) {
		if (parent == null) {
			return false;
		}

		Element root = findRootElementOrNull(parent, attribute);
		return root != null && isAttachedToDom(root) && isRenderedElementSingleChild(root);
	}

	/**
	 * Checks that the parent of {@code rendered} has a single child.
	 */
	private static boolean isRenderedElementSingleChild(Element rendered) {
		return GWT.isProdMode() || rendered.getParentElement().getChildCount() == 1;
	}

	/**
	 * Holds the part of the id attribute common to all elements being rendered.
	 */
	protected String uiId;

	@Override
	public boolean isParentOrRenderer(Element parent) {
		return isParentOrRenderer(parent, RENDERED_ATTRIBUTE);
	}
}
